public class SnowFlow {

    private long workerId;
    private long datacenterId;
    private long sequence;
    private long lastTimestamp = -1L;

    private final long twepoch = 1582201220000L;  // 2020-02-20 20:20:20

    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;

    // max is 31
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    // bit shift
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // max is 4095: 1111 1111 0000 0000 0000 0000  ^  1111 1111 1111 1111 1111 1111  =  0000 0000 1111 1111 1111 1111
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);


    private long timeGen() {
        return System.currentTimeMillis();
    }

    // next timestamp
    private long tillNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    public SnowFlow() {
        this.workerId = 0;
        this.datacenterId = 0;
        this.sequence = 0;
    }

    public SnowFlow(long workerId, long datacenterId, long sequence) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }


    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tillNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        lastTimestamp = timestamp;

        // generated is positive, so the first 1 bit is uniformly 0
        // 64bit(id) = 1bit(zero-symbol) + 41bit(timestamp) + 5bit(datacenterId) + 5bit(workerId) + 12bit(sequence)
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
    }


    public static void main(String[] args) {
        SnowFlow snowFlow = new SnowFlow(0, 0, 0);
        long sf = snowFlow.nextId();
        System.out.println(sf);
    }
}
